package tech.mhuang.pacebox.core.codec;


import java.util.Arrays;

/**
 * base36编码
 * @author mhuang
 * @since 1.1.6
 */
public class Base32 implements Encoder<byte[], String>, Decoder<CharSequence, byte[]> {

    private final String DEFAULT_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
    private final String HEX_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUV";
    private final Character DEFAULT_PAD = '=';
    private static char BASE_CHAR = '0';
    private final int[] BASE32_FILL = {-1, 4, 1, 6, 3};
    private final byte[] lookupTable;


    public static final Base32 CODEC_LOWER = new Base32(true);
    public static final Base32 CODEC_UPPER = new Base32(false);

    private final char[] alphabet;
    private final String alphabetStr;
    private final Character pad;

    public Base32(boolean useHex) {
        if (useHex) {
            this.alphabet = DEFAULT_ALPHABET.toCharArray();
            this.alphabetStr = DEFAULT_ALPHABET;
            this.pad = DEFAULT_PAD;
        } else {
            this.alphabet = HEX_ALPHABET.toCharArray();
            this.alphabetStr = HEX_ALPHABET;
            this.pad = DEFAULT_PAD;
        }
        lookupTable = new byte[128];
        Arrays.fill(lookupTable, (byte) -1);

        final int length = alphabetStr.length();

        char c;
        for (int i = 0; i < length; i++) {
            c = alphabetStr.charAt(i);
            lookupTable[c - BASE_CHAR] = (byte) i;
            // 支持小写字母解码
            if (c >= 'A' && c <= 'Z') {
                lookupTable[Character.toLowerCase(c) - BASE_CHAR] = (byte) i;
            }
        }
    }

    @Override
    public byte[] decode(CharSequence data) {
        int i, index, lookup, offset, digit;
        final String base32 = data.toString();
        int len = base32.endsWith("=") ? base32.indexOf("=") * 5 / 8 : base32.length() * 5 / 8;
        byte[] bytes = new byte[len];

        for (i = 0, index = 0, offset = 0; i < base32.length(); i++) {
            lookup = base32.charAt(i) - BASE_CHAR;

            /* Skip chars outside the lookup table */
            if (lookup < 0 || lookup >= lookupTable.length) {
                continue;
            }

            digit = lookupTable[lookup];

            /* If this digit is not in the table, ignore it */
            if (digit < 0) {
                continue;
            }

            if (index <= 3) {
                index = (index + 5) % 8;
                if (index == 0) {
                    bytes[offset] |= digit;
                    offset++;
                    if (offset >= bytes.length) {
                        break;
                    }
                } else {
                    bytes[offset] |= digit << (8 - index);
                }
            } else {
                index = (index + 5) % 8;
                bytes[offset] |= (digit >>> index);
                offset++;

                if (offset >= bytes.length) {
                    break;
                }
                bytes[offset] |= digit << (8 - index);
            }
        }
        return bytes;
    }

    @Override
    public String encode(byte[] data) {
        int i = 0;
        int index = 0;
        int digit;
        int currByte;
        int nextByte;

        int encodeLen = data.length * 8 / 5;
        if (encodeLen != 0) {
            encodeLen = encodeLen + 1 + BASE32_FILL[(data.length * 8) % 5];
        }

        StringBuilder base32 = new StringBuilder(encodeLen);

        while (i < data.length) {
            // unsign
            currByte = (data[i] >= 0) ? data[i] : (data[i] + 256);

            /* Is the current digit going to span a byte boundary? */
            if (index > 3) {
                if ((i + 1) < data.length) {
                    nextByte = (data[i + 1] >= 0) ? data[i + 1] : (data[i + 1] + 256);
                } else {
                    nextByte = 0;
                }

                digit = currByte & (0xFF >> index);
                index = (index + 5) % 8;
                digit <<= index;
                digit |= nextByte >> (8 - index);
                i++;
            } else {
                digit = (currByte >> (8 - (index + 5))) & 0x1F;
                index = (index + 5) % 8;
                if (index == 0) {
                    i++;
                }
            }
            base32.append(alphabet[digit]);
        }

        if (null != pad) {
            // 末尾补充不足长度的
            while (base32.length() < encodeLen) {
                base32.append(pad.charValue());
            }
        }

        return base32.toString();
    }
}
